Skip to content

fix(sdk): select gRPC native library by platform and arch - #2029

Open
Utkal059 wants to merge 3 commits into
juspay:mainfrom
Utkal059:fix/js-sdk-arm64-grpc-native-lib
Open

fix(sdk): select gRPC native library by platform and arch#2029
Utkal059 wants to merge 3 commits into
juspay:mainfrom
Utkal059:fix/js-sdk-arm64-grpc-native-lib

Conversation

@Utkal059

@Utkal059 Utkal059 commented Aug 1, 2026

Copy link
Copy Markdown

Follow-up to #1940, which reported two arch-blind native loaders in the JavaScript SDK. #1996 fixed the UniFFI one — it resolves generated/<platform>-<arch>/ now, and the release workflow builds aarch64-unknown-linux-gnu. The gRPC loader was left as it was:

// sdk/javascript/src/payments/_generated_grpc_client.ts
const ext = process.platform === "darwin" ? "dylib" : "so";
libPath = path.join(_dirname, "generated", `libhyperswitch_grpc_ffi.${ext}`);

No process.arch, and a flat path with nowhere to put a second architecture. This brings it in line.

Changes

  • sdk/javascript/src/payments/native_lib.ts (new): one resolveNativeLib() shared by both loaders, so the platform/arch mapping can't drift between them.
  • _generated_grpc_client.ts resolves per-arch. The edit is in templates/javascript/grpc_client.ts.j2, regenerated with generate.py --lang grpc; the Python, Kotlin and Rust outputs regenerate byte-identical.
  • make generate-grpc-bindings stages the native under generated/$(NODE_TARGET)/, the way generate-bindings already does, so staging and lookup agree.
  • An unsupported runtime now fails with something actionable. The reporter in Publish an arm64 (aarch64) Linux binary for libconnector_service_ffi #1940 had to run file on the .so to find the mismatch; now it is:
hyperswitch-prism: libconnector_service_ffi is not bundled for linux-s390x
(expected .../generated/linux-s390x/libconnector_service_ffi.so).
This package bundles: darwin-arm64, linux-arm64, linux-x64.

On scope

The gRPC part is consistency rather than a crash anyone hits today: staging and loading were both flat, so they agreed for any single-arch build. It starts to matter once a package carries more than one architecture, which is what make dist now produces for the UniFFI native. The diagnostic is the part users feel immediately.

One thing I ran into and left alone: make -C sdk/javascript dist stages only libconnector_service_ffi, and release-sdks.yml never calls generate-grpc-bindings, so no gRPC native ships in the npm tarball on any platform. Shipping a second native reads as a packaging decision rather than a loader fix, so I have not made it here. Happy to follow up if it is wanted.

Testing

make -C sdk test-grpc in ci.yml covers this path end to end — it stages the native, packs a tarball, installs it, and loads through the resolver.

Locally:

  • tsc --noEmit matches main: the same two pre-existing generated/proto errors, nothing new.
  • Resolver checked against a simulated multi-arch tarball with process.platform and process.arch overridden. Each of linux-arm64, linux-x64 and darwin-arm64 resolves to its own directory and extension; aarch64 never falls through to the x86-64 binary; a stray flat generated/*.so is ignored; both failure paths produce the message above.
  • make -C sdk/javascript generate-grpc-bindings stages into generated/linux-x64/.

I do not have aarch64 hardware, so this has not run against a real Graviton box — @alondayan2 offered to test an arm64 build in #1940.

Refs #1940

The JavaScript SDK's UniFFI loader resolves natives from
generated/<platform>-<arch>/, but the gRPC loader still selected on
process.platform alone and read a flat generated/ path, so it had no way
to pick between architectures. Route both loaders through one resolver
and stage the gRPC native under the same per-arch directory.

When no matching binary is bundled, the resolver now reports the target
it looked for and the targets the package actually ships, instead of
letting dlopen fail with a "wrong ELF class" message that does not say
which architecture was expected.
@Utkal059
Utkal059 requested a review from a team as a code owner August 1, 2026 17:02
Copilot AI review requested due to automatic review settings August 1, 2026 17:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aligns the JavaScript SDK’s native library loading behavior with the multi-arch packaging layout by resolving native binaries from generated/<platform>-<arch>/, and centralizes the platform/arch resolution logic to prevent drift between UniFFI and gRPC loaders.

Changes:

  • Introduces a shared resolveNativeLib() resolver for platform+arch-aware native library lookup and better failure diagnostics.
  • Updates the gRPC generated client (and its template) to use the shared resolver and the per-arch directory layout.
  • Updates the JavaScript SDK Makefile to stage the gRPC native library under generated/$(NODE_TARGET)/ to match runtime lookup.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
sdk/javascript/src/payments/uniffi_client.ts Switches UniFFI native loading to the shared resolver and per-arch layout.
sdk/javascript/src/payments/native_lib.ts Adds a shared platform+arch native resolver with improved error messaging.
sdk/javascript/src/payments/_generated_grpc_client.ts Updates gRPC native loading to use the shared resolver and per-arch layout.
sdk/javascript/Makefile Stages gRPC native libs into generated/<platform>-<arch>/ to match resolver behavior.
scripts/generators/code/templates/javascript/grpc_client.ts.j2 Updates the generator template so regenerated gRPC clients use the shared resolver.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +20 to +23
export function resolveNativeLib(generatedDir: string, libName: string): string {
const target = `${process.platform}-${process.arch}`;
const ext = process.platform === "darwin" ? "dylib" : "so";
const libPath = path.join(generatedDir, target, `${libName}.${ext}`);
The SDK requirements list Windows (x64), but the extension mapping only
distinguished darwin from everything else, so a win32 runtime reported a
.so path it would never have loaded. Map win32 to .dll so the "not
bundled" diagnostic names the file the platform actually uses.

No behaviour change on Linux or macOS, and no Windows native is built
today — this only affects which filename the error reports.
@Utkal059

Utkal059 commented Aug 1, 2026

Copy link
Copy Markdown
Author

CI note on the failing SDK Tests check: it stops at Credentials file not found: creds.json. The job log reports Secret source: None — a PR from a fork doesn't receive CONNECTOR_SPECIFIC_AUTH, so the "Create connector credentials" step writes nothing and the Rust and Kotlin gRPC smoke tests can't start. The JavaScript and Python harnesses fall back to placeholder credentials, and both passed:

══ gRPC Results Summary ══
  ✗ Rust (exit 2)
  ✓ JavaScript
  ✓ Python
  ✗ Kotlin (exit 2)

JavaScript is the only SDK this PR touches, and that run exercises the changed path end to end — generate-grpc-bindings stages the native, pack-archive builds the tarball, the test installs it, and the client loads through the new resolver.

The diff touches no Rust, Kotlin, or workflow files, so I've left CI alone rather than working around the missing credentials. Happy to rebase or re-run whenever it suits.

@XyneSpaces

Copy link
Copy Markdown

[blocking] The JavaScript package manifest still only includes dist/src/payments/generated/*, but this PR moves native libraries into generated/<platform>-<arch>/.... npm's files glob will not include those nested directories, so a published package can install without the native library and resolveNativeLib will fail at runtime; please update sdk/javascript/package.json to include the recursive generated tree (for example dist/src/payments/generated/**/*).

@XyneSpaces

Copy link
Copy Markdown

Automated review note: I found line-specific issues in this PR, but this environment only exposes PR-level comment creation and not inline diff-comment posting. Per review policy, I’m not bundling line-specific findings into a PR-level comment.

@XyneSpaces

Copy link
Copy Markdown

[blocking] dist still stages only libconnector_service_ffi, but _generated_grpc_client.ts now resolves generated/<platform>-<arch>/libhyperswitch_grpc_ffi.*. Please update the distribution packaging loop to stage libhyperswitch_grpc_ffi for every supported target too; otherwise the published JS package will fail to load the gRPC client on every platform.

dist staged only libconnector_service_ffi, so the gRPC client resolved a
path no published tarball ever contained. Stage both natives per target
via JS_NATIVE_LIBS.

The release workflow built only libconnector_service_ffi, so staging the
gRPC native without touching it would have tripped the existing
fail-closed check and aborted every release. Build hyperswitch-grpc-ffi
and upload libhyperswitch_grpc_ffi.* alongside it.

package.json needs no change: npm-packlist includes a matched directory
recursively, so the existing generated/* entry already ships the per-arch
subdirectories. Verified against a real dist tarball.
@Utkal059

Utkal059 commented Aug 2, 2026

Copy link
Copy Markdown
Author

Pushed fc83e72 — thanks for both. I think the second one holds and the first doesn't; evidence for each below.

dist staging the gRPC native — correct, and fixed.

One wrinkle the suggestion doesn't cover: release-sdks.yml builds and uploads only libconnector_service_ffi, so staging libhyperswitch_grpc_ffi in dist alone would have hit the existing fail-closed check and aborted every release run. The commit does both — JS_NATIVE_LIBS in the staging loop, plus a build-grpc-ffi-lib step and a widened upload glob in the workflow. A published package now carries a gRPC native for the first time.

package.json files glob — I don't think this one is real, so I've left it unchanged.

npm-packlist includes a matched directory recursively, so dist/src/payments/generated/* already covers generated/<platform>-<arch>/. Three npm pack runs over identical trees:

files entry natives packed
generated/* (current) all 6
generated line removed none — confirms that entry is what ships them
generated/**/* (suggested) identical to current

And from a real make -C sdk/javascript dist tarball, with package.json untouched:

package/dist/src/payments/generated/darwin-arm64/libconnector_service_ffi.dylib
package/dist/src/payments/generated/darwin-arm64/libhyperswitch_grpc_ffi.dylib
package/dist/src/payments/generated/linux-arm64/libconnector_service_ffi.so
package/dist/src/payments/generated/linux-arm64/libhyperswitch_grpc_ffi.so
package/dist/src/payments/generated/linux-x64/libconnector_service_ffi.so
package/dist/src/payments/generated/linux-x64/libhyperswitch_grpc_ffi.so

Worth noting the per-arch layout isn't new in this PR — #1996 shipped it for the UniFFI native, so a glob that couldn't reach those subdirectories would already be visible in released packages today.

Happy to switch to **/* regardless if you'd prefer it as explicit documentation of intent, but it's a no-op.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants